/*
    TreeViewer - Cross-platform software to draw phylogenetic trees
    Copyright (C) 2023  Giorgio Bianchini, University of Bristol
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using PhyloTree;
using TreeViewer;
using VectSharp;
using VectSharp.Markdown;
using System.Runtime.InteropServices;

namespace a06888353e9304d08ab245727bced8cd6
{
    /// <summary>
    /// This module renders a snippet of [Markdown](https://en.wikipedia.org/wiki/Markdown) code and draws it on the plot.
    /// This can be used e.g. to display a description or legend of the plot.
    /// </summary>
    /// 
    /// <description>
    /// <![CDATA[
    /// ## Further information
    /// 
    /// In addition to the [Markdown features supported by VectSharp.Markdown](https://github.com/arklumpus/VectSharp/tree/master/VectSharp.Markdown#supported-markdown-features),
    /// this module supports some special image protocols to draw coloured symbols that may aid in creating a legend for
    /// the tree plot.
    /// 
    /// These are used by inserting in the markdown source an image whose address starts with `attachment://`, `circle://`, `ellipse://`,
    /// `square://`, `rect://`, `poly://` or `star://`. For example:
    /// 
    /// ```Markdown
    /// ![](star://8,blue) Blue star
    /// ```
    /// 
    /// Renders to:
    /// 
    /// <img height="50" src="" />
    ///
    /// After the special protocol, you need to supply a series of parameters that will be used to determine the size
    /// and colour of the shape that is drawn:
    /// 
    /// * `attachment://<name>`
    /// 
    ///     This includes the image contained in the attachment with the specified name in the Markdown document. The
    ///     correct format for the image is chosen automatically.
    ///
    ///     Example:
    ///
    ///     * `attachment://Legend`    
    /// 
    /// * `circle://<diameter>,<colour>[,<stroke thickness>[,<stroke colour>]]`
    /// 
    ///     This draws a circle with the specified diameter and colour. The `stroke thickness` and `stroke colour`
    ///     parameters can be omitted, in which case the circle will only be filled and not stroked.
    /// 
    ///     Examples:
    /// 
    ///     * `circle://8,cornflowerblue` <img height="50" src="" />
    ///     * `circle://6,#009e76,1,black` <img height="50" src="" />
    ///     * `circle://10,,1,orange` <img height="50" src="" />
    /// 
    /// * `ellipse://<width>,<height>,<colour>[,<stroke thickness>[,<stroke colour>]]`
    /// 
    ///     This draws an ellipse with the specified width, height and colour. The `stroke thickness` and `stroke colour`
    ///     parameters can be omitted, in which case the ellipse will only be filled and not stroked.
    /// 
    ///     Examples:
    /// 
    ///     * `ellipse://12,8,cornflowerblue` <img height="50" src="" />
    ///     * `ellipse://10,6,#009e76,1,black` <img height="50" src="" />
    ///     * `ellipse://8,10,,1,orange` <img height="50" src="" />
    /// 
    /// * `square://<size>,<colour>[,<stroke thickness>[,<stroke colour>]]`
    /// 
    ///     This draws a square with the specified size and colour. The `stroke thickness` and `stroke colour`
    ///     parameters can be omitted, in which case the square will only be filled and not stroked.
    /// 
    ///     Examples:
    /// 
    ///     * `square://8,cornflowerblue` <img height="50" src="" />
    ///     * `square://6,#009e76,1,black` <img height="50" src="" />
    ///     * `square://10,,1,orange` <img height="50" src="" />
    /// 
    /// * `rect://<width>,<height>,<colour>[,<stroke thickness>[,<stroke colour>]]`
    /// 
    ///     This draws a rectangle with the specified width, height and colour. The `stroke thickness` and `stroke colour`
    ///     parameters can be omitted, in which case the rectangle will only be filled and not stroked.
    /// 
    ///     Examples:
    /// 
    ///     * `rect://12,8,cornflowerblue` <img height="50" src="" />
    ///     * `rect://10,6,#009e76,1,black` <img height="50" src="" />
    ///     * `rect://8,10,,1,orange` <img height="50" src="" />
    /// 
    /// * `poly://<width>[,<height>],<colour>[,<sides>[,<stroke thickness>[,<stroke colour>]]]`
    /// 
    ///     This draws a polygon with the specified width, height, colour and number of sides. The `stroke thickness`
    ///     and `stroke colour` parameters can be omitted, in which case the polygon will only be filled and not stroked.
    ///     If the `height` is omitted, it will be equal to the `width`. If the number of `sides` is omitted, it will be
    ///     equal to `3`.
    /// 
    ///     Examples:
    /// 
    ///     * `poly://8,cornflowerblue` <img height="50" src="" />
    ///     * `poly://10,6,#009e76,5,1,black` <img height="50" src="" />
    ///     * `poly://8,10,,4,1,orange` <img height="50" src="" />
    /// 
    /// * `star://<width>[,<height>],<colour>[,<tips>[,<stroke thickness>[,<stroke colour>]]]`
    /// 
    ///     This draws a star with the specified width, height, colour and number of tips. The `stroke thickness`
    ///     and `stroke colour` parameters can be omitted, in which case the star will only be filled and not stroked.
    ///     If the `height` is omitted, it will be equal to the `width`. If the number of `sides` is omitted, it will be
    ///     equal to `5`.
    /// 
    ///     Examples:
    /// 
    ///     * `star://8,cornflowerblue` <img height="50" src="" />
    ///     * `star://10,6,#009e76,3,1,black` <img height="50" src="" />
    ///     * `star://8,10,,4,1,orange` <img height="50" src="" />
    /// ]]>
    /// </description>
    public static class MyModule
    {
        public const string Name = "Legend";
        public const string HelpText = "Draws a Markdown legend.";
        public const string Author = "Giorgio Bianchini";
        public static Version Version = new Version("1.0.0");
        public const ModuleTypes ModuleType = ModuleTypes.Plotting;

        public const string Id = "06888353-e930-4d08-ab24-5727bced8cd6";
		
		private static string Icon16Base64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACFSURBVDhPY/Sq3fifAQ1sa/ZnhDIJAiYoTTbA64KioiIMOXQw8F6gGIC84ACkS4DYG4i3AnEP0AsHgDRRAOQFmGYQANEgPtGAKrGwBUjDXAACW4EG+EDZBAHICz1ADPI7CIDDAMKkEwD7FRoT+4HYkZQYAAFYQgJpRqaJBngDkQ55gYEBAFoxNZFPm+qKAAAAAElFTkSuQmCC";
        private static string Icon24Base64 = "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAClSURBVEhLY6A1YAQRXrUb/4N5aGBbsz9YnhLABKVpBojyQVFREVZ5QqCvr49xcPiAEkBzHwx9AA9jYDw0AKl6CI+hERj+ID7FgBlEoBkOAg6q9pGMtw+tOADlkw2GTz5ADyIQoEo8gOMAFNagMAcyHUB8IKBaJA8zAIoLaHxQDYDjAASQIppqeQAEkHMySlpHLkkpzgfYgoVaQQXLB6P1AQ7AwAAAjYtJfqvEEngAAAAASUVORK5CYII=";
        private static string Icon32Base64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADbSURBVFhHYxhowAgivGo3/gfzcIBtzf5gdbQATFB6wMCAO4DkKCgqKsKrlhTQ19fHOPRCgNpgNBGOggEHKKkbmBsagFQ9hMfQCEz9ID5NAdwBQMu3AClvCA8OtgId4QNl0wSAcwHU5+iWg4A3VI5mgOSCaNgWxciJDx3QNDEOjkQIAlCLGiE8MAD5nKaWj4LBCUA5gtaFDzJghtJggJQdHVTtIxlvH1pxACxBQ4BeECGXBbjKBaoC5HIAW2GEUQjRsijG5mOah8KAF8WwEMDnU5qGwoDXhgMMGBgAt8lQukQ7VMUAAAAASUVORK5CYII=";

        public static Page GetIcon(double scaling)
        {
            byte[] bytes;

            if (scaling <= 1)
            {

                bytes = Convert.FromBase64String(Icon16Base64);
            }
            else if (scaling <= 1.5)
            {
                bytes = Convert.FromBase64String(Icon24Base64);
            }
            else
            {
                bytes = Convert.FromBase64String(Icon32Base64);
            }

            IntPtr imagePtr = Marshal.AllocHGlobal(bytes.Length);
            Marshal.Copy(bytes, 0, imagePtr, bytes.Length);

            RasterImage icon;

            try
            {
                icon = new VectSharp.MuPDFUtils.RasterImageStream(imagePtr, bytes.Length, MuPDFCore.InputFileTypes.PNG);
            }
            catch (Exception ex)
            {
                throw ex.InnerException;
            }
            finally
            {
                Marshal.FreeHGlobal(imagePtr);
            }

            Page pag = new Page(16, 16);
            pag.Graphics.DrawRasterImage(0, 0, 16, 16, icon);

            return pag;
        }

        public static List<(string, string)> GetParameters(TreeNode tree)
        {
            List<string> leafNames = tree.GetLeafNames();

            return new List<(string, string)>()
            {
                ( "StateData", "InstanceStateData:" ),

                /// <param name="Markdown source:">
                /// This parameter is used to specify the Markdown source code for the legend that is drawn on the plot.
                /// See [Further information](#further-information) for instructions on how to include some common
                /// symbols in this; if you wish to include more complicated things, you can create them in a graphics
                /// software, include them as attachments, and use the `attachment://` syntax.
                /// </param>
                ( "Markdown source:", "Markdown:### **Legend**\n\n* ![](circle://8,#CC79A7) Red circle\n* ![](square://8,#0072B2) Blue square\n* ![](star://8,#009E73) Green star" ),

                /// <param name="Background colour:">
                /// The background colour for the legend.
                /// </param>
                ( "Background colour:", "Colour:[255, 255, 255, 255]" ),

                ( "Text", "Group:2" ),
                
                /// <param name="Font size:">
                /// The base font size used to render the legend. Headers use a larger font.
                /// </param>
                ( "Font size:", "NumericUpDown:14[\"0\", \"Infinity\"]" ),
                
                /// <param name="Colour:  " display="Text colour">
                /// The colour of the text in the legend.
                /// </param>
                ( "Colour:  ", "Colour:[0, 0, 0, 255]" ),

                ( "Layout", "Group:2" ),
                
                /// <param name="Width:">
                /// The width of the legend (the height is computed automatically).
                /// </param>
                ( "Width:", "NumericUpDown:200[\"0\", \"Infinity\"]" ),

                ( "Margin", "Group:4" ),
                
                /// <param name="Left:" display="Left margin">
                /// The left margin of the legend. Note that this can be &lt; 0.
                /// </param>
                ( "Left:", "NumericUpDown:5[\"-Infinity\", \"Infinity\"]" ),
                
                /// <param name="Top:" display="Top margin">
                /// The top margin of the legend. Note that this can be &lt; 0.
                /// </param>
                ( "Top:", "NumericUpDown:5[\"-Infinity\", \"Infinity\"]" ),
                
                /// <param name="Right:" display="Right margin">
                /// The right margin of the legend. Note that this can be &lt; 0.
                /// </param>
                ( "Right:", "NumericUpDown:5[\"-Infinity\", \"Infinity\"]" ),
                
                /// <param name="Bottom:" display="Bottom margin">
                /// The bottom margin of the legend. Note that this can be &lt; 0, which is useful to counter excessive space
                /// after the last text line.
                /// </param>
                ( "Bottom:", "NumericUpDown:-5[\"-Infinity\", \"Infinity\"]" ),

                ( "Border", "Group:2" ),
                
                /// <param name="Colour: " display="Border colour">
                /// The colour of the border drawn around the legend.
                /// </param>
                ( "Colour: ", "Colour:[0, 0, 0, 255]" ),
                
                /// <param name="Thickness: " display="Border thickness">
                /// The thickness of the border drawn around the legend.
                /// </param>
                ( "Thickness: ", "NumericUpDown:0[\"0\", \"Infinity\"]" ),

                ( "Header lines", "Group:2" ),
                
                /// <param name="Colour:" display="Header line colour">
                /// The colour of the line drawn below headers in the legend.
                /// </param>
                ( "Colour:", "Colour:[180, 180, 180, 255]" ),
                
                /// <param name="Thickness:" display="Header line thickness">
                /// The thickness of the line drawn below headers in the legend.
                /// </param>
                ( "Thickness:", "NumericUpDown:0[\"0\", \"Infinity\"]" ),

                ("Position","Group:4"),
                
                /// <param name="Anchor:">
                /// This parameter is used to select the anchor used to determine the position of the legend. If
                /// the selected value is `Node`, the specified node is used as an anchor. Otherwise, the selected point
                /// on the tree plot is used. Note that these positions refer to the _tree_ plot and do not take into
                /// account the presence of labels and other elements.
                /// </param>
                ("Anchor:","ComboBox:8[\"Node\",\"Top-left\",\"Top-center\",\"Top-right\",\"Middle-left\",\"Middle-center\",\"Middle-right\",\"Bottom-left\",\"Bottom-center\",\"Bottom-right\"]"),
                
                /// <param name="Node:">
                /// If the [Anchor](#anchor) was set to `Node`, this control is used to select the node that acts as an
                /// anchor.
                /// </param>
                ("Node:","Node:[\"" + leafNames[0] +"\",\"" + leafNames[^1] + "\"]"),
                
                /// <param name="Alignment:">
                /// This parameter controls to which point on the legend the selected [Anchor](#anchor) corresponds.
                /// </param>
                ("Alignment:","ComboBox:1[\"Top-left\",\"Top-center\",\"Top-right\",\"Middle-left\",\"Middle-center\",\"Middle-right\",\"Bottom-left\",\"Bottom-center\",\"Bottom-right\"]"),
                
                /// <param name="Position:">
                /// This parameter determines how much the legend is shifted with respect to the position determined
                /// by the [Anchor](#anchor) and the [Alignment](#alignment).
                /// </param>
                ("Position:","Point:[0,10]"),

            };
        }

        public static bool OnParameterChange(object tree, Dictionary<string, object> previousParameterValues, Dictionary<string, object> currentParameterValues, out Dictionary<string, ControlStatus> controlStatus, out Dictionary<string, object> parametersToChange)
        {
            controlStatus = new Dictionary<string, ControlStatus>();
            parametersToChange = new Dictionary<string, object>();

            if ((int)currentParameterValues["Anchor:"] == 0)
            {
                controlStatus["Node:"] = ControlStatus.Enabled;
            }
            else
            {
                controlStatus["Node:"] = ControlStatus.Hidden;
            }

            return true;
        }

        public static Point[] PlotAction(TreeNode tree, Dictionary<string, object> parameterValues, Dictionary<string, Point> coordinates, Graphics graphics)
        {
            string text = (string)parameterValues["Markdown source:"];

            Colour headerLineColour = (Colour)parameterValues["Colour:"];
            double headerLineThickness = (double)parameterValues["Thickness:"];

            Colour backgroundColour = (Colour)parameterValues["Background colour:"];
            Colour borderColour = (Colour)parameterValues["Colour: "];
            double borderThickness = (double)parameterValues["Thickness: "];

            double width = (double)parameterValues["Width:"];
            double marginLeft = (double)parameterValues["Left:"];
            double marginTop = (double)parameterValues["Top:"];
            double marginRight = (double)parameterValues["Right:"];
            double marginBottom = (double)parameterValues["Bottom:"];

            double fontSize = (double)parameterValues["Font size:"];
            Colour textColour = (Colour)parameterValues["Colour:  "];

            InstanceStateData stateData = (InstanceStateData)parameterValues["StateData"];

            TreeNode anchorNode = (tree.GetLastCommonAncestor((string[])parameterValues["Node:"]) ?? tree);

            int anchorType = (int)parameterValues["Anchor:"];
            int alignment = (int)parameterValues["Alignment:"];

            double plotMaxX = double.MinValue;
            double plotMinX = double.MaxValue;
            double plotMaxY = double.MinValue;
            double plotMinY = double.MaxValue;

            foreach (KeyValuePair<string, Point> kvp in coordinates)
            {
                if (tree.GetNodeFromId(kvp.Key) != null)
                {
                    plotMaxX = Math.Max(plotMaxX, kvp.Value.X);
                    plotMaxY = Math.Max(plotMaxY, kvp.Value.Y);
                    plotMinX = Math.Min(plotMinX, kvp.Value.X);
                    plotMinY = Math.Min(plotMinY, kvp.Value.Y);
                }
            }

            double anchorX = coordinates[Modules.RootNodeId].X;
            double anchorY = coordinates[Modules.RootNodeId].Y;

            if (anchorType == 0)
            {
                Point anchor = coordinates[anchorNode.Id];
                anchorX = anchor.X;
                anchorY = anchor.Y;
            }
            else
            {
                if (anchorType == 1 || anchorType == 2 || anchorType == 3)
                {
                    anchorY = plotMinY;
                }
                else if (anchorType == 4 || anchorType == 5 || anchorType == 6)
                {
                    anchorY = (plotMinY + plotMaxY) * 0.5;
                }
                else if (anchorType == 7 || anchorType == 8 || anchorType == 9)
                {
                    anchorY = plotMaxY;
                }

                if (anchorType == 1 || anchorType == 4 || anchorType == 7)
                {
                    anchorX = plotMinX;
                }
                else if (anchorType == 2 || anchorType == 5 || anchorType == 8)
                {
                    anchorX = (plotMinX + plotMaxX) * 0.5;
                }
                else if (anchorType == 3 || anchorType == 6 || anchorType == 9)
                {
                    anchorX = plotMaxX;
                }
            }

            Point position = (Point)parameterValues["Position:"];


            MarkdownRenderer renderer = MarkdownUtils.GetMarkdownRenderer(stateData);
            renderer.HeaderLineColour = headerLineColour;
            for (int i = 0; i < renderer.HeaderLineThicknesses.Length; i++)
            {
                renderer.HeaderLineThicknesses[i] *= headerLineThickness;
            }
            renderer.Margins = new Margins(marginLeft, marginTop, marginRight, marginBottom);
            renderer.BackgroundColour = backgroundColour;
            renderer.BaseFontSize = fontSize;
            renderer.ImageMultiplier = fontSize / 10.5;
            renderer.ForegroundColour = textColour;
            renderer.ThematicBreakLineColour = headerLineColour;

            Page pag = renderer.RenderSinglePage(text, width, out _);

            double plotPosX = 0;
            double plotPosY = 0;

            if (alignment == 0 || alignment == 1 || alignment == 2)
            {
                plotPosY = 0;
            }
            else if (alignment == 3 || alignment == 4 || alignment == 5)
            {
                plotPosY = -pag.Height * 0.5;
            }
            else if (alignment == 6 || alignment == 7 || alignment == 8)
            {
                plotPosY = -pag.Height;
            }

            if (alignment == 0 || alignment == 3 || alignment == 6)
            {
                plotPosX = 0;
            }
            else if (alignment == 1 || alignment == 4 || alignment == 7)
            {
                plotPosX = -pag.Width * 0.5;
            }
            else if (alignment == 2 || alignment == 5 || alignment == 8)
            {
                plotPosX = -pag.Width;
            }

            if (backgroundColour.A > 0)
            {
                graphics.FillRectangle(position.X + anchorX + plotPosX, position.Y + anchorY + plotPosY, pag.Width, pag.Height, backgroundColour);
            }

            if (borderThickness > 0 && borderColour.A > 0)
            {
                graphics.StrokeRectangle(position.X + anchorX + plotPosX, position.Y + anchorY + plotPosY, pag.Width, pag.Height, borderColour, borderThickness);
            }

            graphics.DrawGraphics(position.X + anchorX + plotPosX, position.Y + anchorY + plotPosY, pag.Graphics);

            Point topLeft = new Point(position.X + anchorX + plotPosX - borderThickness, position.Y + anchorY + plotPosY - borderThickness);
            Point bottomRight = new Point(topLeft.X + pag.Width + borderThickness, topLeft.Y + pag.Height + borderThickness);

            return new Point[] { topLeft, bottomRight };
        }
    }
}